﻿using System;
using System.Runtime.CompilerServices;
using System.Threading;
using CoreLibrary.Service;
using NLog;
using Quartz;
using Quartz.Impl;
using Quartz.Impl.Triggers;

namespace CoreLibrary.Scheduler {

    public class SchedulerService : ServiceBase, ISchedulerService {

        private DirectSchedulerFactory _quartzSchedulerFactory;
        private IScheduler _quartzScheduler;

        private const string ArgumentKey  = "__Argument__";
        private const string SchedulerKey = "__Scheduler__";
        private const string ActionKey    = "__Action__";

        private int _triggerCount;
        private int _jobCount;

        public ExceptionHandler UncaughtExceptionHandler { get; set; }

        protected override void OnInit() {
            _quartzSchedulerFactory = DirectSchedulerFactory.Instance;
            _quartzSchedulerFactory.CreateVolatileScheduler(10);
            _quartzScheduler = _quartzSchedulerFactory.GetScheduler();
            _quartzScheduler.Start();
        }

        protected override void OnStart() {
            _quartzScheduler.ResumeAll();
        }

        protected override void OnStop() {
            _quartzScheduler.PauseAll();
        }

        protected override void OnDestroy() {
            _quartzScheduler.Shutdown();
        }

        public ICancelToken Schedule(Action action, TimeSpan delay) {
            return Schedule(n => action(), null, delay);
        }

        public ICancelToken Schedule(Action action, ITrigger trigger) {
            return Schedule(n => action(), null, trigger);
        }

        public ICancelToken Schedule(Action<object> action, object argument, TimeSpan delay) {
            return Schedule(action, argument, new SimpleTriggerImpl(
                   GetNextTriggerName(),
                   null,
                   DateTime.UtcNow.Add(delay),
                   null,
                   0,
                   TimeSpan.Zero
            ));
        }

        public ICancelToken Schedule(Action<object> action, object argument, ITrigger trigger) {
            var job = new JobDetailImpl(GetNextJobName(), null, typeof(SimpleJob));
            job.JobDataMap[SchedulerKey] = this;
            job.JobDataMap[ActionKey]    = action;
            job.JobDataMap[ArgumentKey]  = argument;
            _quartzScheduler.ScheduleJob(job, trigger);
            return new TriggerFuture(_quartzScheduler, trigger);
        }

        public ICancelToken ScheduleFixedDelay(Action<CancellationDelegate> action, TimeSpan initialDelay, TimeSpan interval) {
            return ScheduleFixedDelay((n, c) => action(c), null, initialDelay, interval);
        }

        public ICancelToken ScheduleFixedDelay(Action<object, CancellationDelegate> action, object argument, TimeSpan initialDelay, TimeSpan interval) {
            var job   = new FixedDelayJob(this, action, interval);
            var timer = new Timer(job.Run, argument, TimeSpan.FromMilliseconds(-1), TimeSpan.FromMilliseconds(-1));
            job.Timer = timer;
            timer.Change(initialDelay, TimeSpan.FromMilliseconds(-1));
            return job;
        }

        public ICancelToken ScheduleFixedPeriod(Action<CancellationDelegate> action, TimeSpan initialDelay, TimeSpan interval) {
            return ScheduleFixedPeriod((n, c) => action(c), null, initialDelay, interval);
        }

        public ICancelToken ScheduleFixedPeriod(Action<object, CancellationDelegate> action, object argument, TimeSpan initialDelay, TimeSpan interval) {
            var trigger = new SimpleTriggerImpl(
                GetNextTriggerName(),
                null,
                DateTime.UtcNow.Add(initialDelay),
                null,
                SimpleTriggerImpl.RepeatIndefinitely,
                interval
            );

            var job = new JobDetailImpl(GetNextJobName(), null, typeof(FixedPeriodJob));
            job.JobDataMap[SchedulerKey] = this;
            job.JobDataMap[ActionKey]    = action;
            job.JobDataMap[ArgumentKey]  = argument;

            _quartzScheduler.ScheduleJob(job, trigger);
            return new TriggerFuture(_quartzScheduler, trigger);
        }

        public void Unschedule(ITrigger trigger) {
            _quartzScheduler.UnscheduleJob(trigger.Key);
        }

        private void ProcessJobError(Exception error) {
            if (UncaughtExceptionHandler != null) {
                try {
                    UncaughtExceptionHandler(error);
                } 
                catch (Exception failture) {
                    LogManager.GetCurrentClassLogger().ErrorException("Scheduler uncaught exception handling failed:" + failture.Message, failture);
                }
            } 
            else {
                LogManager.GetCurrentClassLogger().ErrorException("Executing simple job failed:" + error.Message, error);
            }
        }

        private string GetNextJobName() {
            return "Scheduler.Job#" + Interlocked.Increment(ref _jobCount);
        }

        private string GetNextTriggerName() {
            return "Scheduler.Trigger#" + Interlocked.Increment(ref _triggerCount);
        }

        private class SimpleJob : IJob {

            public void Execute(IJobExecutionContext context) {
                var data = context.JobDetail.JobDataMap;
                try {
                    ((Action<object>)data.Get(ActionKey))(data.Get(ArgumentKey));
                } 
                catch (Exception e) {
                    ((SchedulerService)data.Get(SchedulerKey)).ProcessJobError(e);
                }
            }
        }

        private class FixedDelayJob : ICancelToken {

            public Timer Timer { get; set; }

            private readonly SchedulerService _schedulerService;
            private readonly Action<object, CancellationDelegate> _action;
            private readonly TimeSpan _interval;

            private bool _cancelled;

            public FixedDelayJob(SchedulerService schedulerService, 
                                 Action<object, CancellationDelegate> action, 
                                 TimeSpan interval) {
                _action = action;
                _schedulerService = schedulerService;
                _interval = interval;
                _cancelled = false;
            }

            public void Cancel() {
                _cancelled = true;
            }

            public bool IsCancelled() {
                return _cancelled;
            }

            public void Run(object state) {
                if (_cancelled) {
                    return;
                }

                int i = 0;
                try {
                    _action(state, () => {
                        Interlocked.CompareExchange(ref i, 1, 0);
                        Timer.Dispose();
                    });
                } catch (Exception e) {
                    _schedulerService.ProcessJobError(e);
                }

                if (Interlocked.CompareExchange(ref i, 2, 0) == 0) {
                    Timer.Change(_interval, TimeSpan.FromMilliseconds(-1));
                }
            }
        }

        private class FixedPeriodJob : IJob {

            public void Execute(IJobExecutionContext context) {
                var data = context.JobDetail.JobDataMap;
                try {
                    var action = (Action<object, CancellationDelegate>)data.Get(ActionKey);
                    action(data.Get(ArgumentKey), () => context.Scheduler.UnscheduleJob(context.Trigger.Key));
                } 
                catch (Exception e) {
                    LogManager.GetCurrentClassLogger().ErrorException("Executing Quartz fixed period job failed:" + e.Message, e);
                }
            }

        }

        public class TriggerFuture : ICancelToken {

            private readonly IScheduler _scheduler;
            private readonly ITrigger _trigger;
            private bool _cancelled;

            public TriggerFuture(IScheduler scheduler, ITrigger trigger) {
                _scheduler = scheduler;
                _trigger = trigger;
                _cancelled = false;
            }

            [MethodImpl(MethodImplOptions.Synchronized)]
            public void Cancel() {
                if (!_cancelled) {
                    _scheduler.UnscheduleJob(_trigger.Key);
                    _cancelled = true;
                }
            }

            [MethodImpl(MethodImplOptions.Synchronized)]
            public bool IsCancelled() {
                return _cancelled;
            }
        }
    }

}